/*
 * Engine Alpha ist eine anfängerorientierte 2D-Gaming Engine.
 *
 * Copyright (c) 2011 - 2014 Michael Andonie and contributors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package ea.internal.phy;

import ea.*;

import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Ein Objekt der Klasse Physik behandelt eigenstaendig verschiedene Raum-Objekte als
 * Physik-Engine.<br /> Grundlegend behandelt sie Trefferkollisionen eigenstaendig, also bietet es
 * sich an, diese Eigenschaft zu nutzen, und zwar in dem Interface <code>KollisionsReagierbar</code>.<br
 * /> <br />
 * <p/>
 * Weiterhin - und dies ist eine <b>essentielle Aufgabe fuer viele 2D-Spiele</b> - kann diese
 * Maschine<br /> - An gewuenschtren Raum-Objekten Schwerkraft erzeugen<br /> - Aktive Raum-Objekte
 * (<b>Aktivobjekte</b>, zum Beispiel Spielfiguren) so beeinlfussen, dass es fuer sie nicht moeglich
 * ist, passive Raum-Objekte (<b>PassivObjekte</b>, zum Beispiel Mauer, Waende, Boeden) zu
 * schneiden.<br /> <br /> Diese beiden Eigenschaften in Kombination erzeugen die Moeglichkeit, sehr
 * einfach eine funktionierende Spielewelt zu programmieren, in der bereits ein funktionierendes
 * System zum Fallen und Grenzen abstecken existiert.<br /> <br />
 *
 * @author Michael Andonie
 */
@SuppressWarnings ( "serial" )
public class Physik extends Manager implements Ticker {

	/**
	 * Die EINE Physik
	 */
	private static Physik physik;

	/**
	 * Die Liste aller Kollisionstestauftraege
	 */
	private ArrayList<Auftrag> kollisionsListe = new ArrayList<>();

	/**
	 * Eine Liste aller Passiven Objekte.
	 */
	private CopyOnWriteArrayList<Passivator> passive = new CopyOnWriteArrayList<>();

	/**
	 * Eine Liste aller Gravitatoren (indirekt Aktivobjekte)
	 */
	private CopyOnWriteArrayList<Gravitator> gravitatoren = new CopyOnWriteArrayList<>();

	/**
	 * Der Rundenzaehler der Physik
	 */
	private int runde = 1;

	/**
	 * Konstruktor.
	 */
	private Physik () {
		super();
		Manager.standard.anmelden(new Ticker() {
			public void tick () {
				for (Auftrag a : kollisionsListe) {
					a.test();
				}
			}
		}, 35);
		this.anmelden(this, 1);
	}

	/**
	 * Neutralisiert die aktuelle Physik und macht Platz fuer eine neue.
	 */
	public static void neutralize () {
		if (physik == null) return;
		// Beendet Berechnungen
		physik.kill();
		for (Passivator p : physik.passive) {
			p.ziel().neutralMachen();
		}
		for (Gravitator g : physik.gravitatoren) {
			g.ziel().neutralMachen();
		}
		physik = null;
	}

	/**
	 * Realisierung eines <i>Singleton</i>. Da es nur eine Physik pro Anwendung gibt, garantiert
	 * diese statische Methode, dass es nur ein Physik Objekt gibt, da keine Physik-Objekte erstellt
	 * werden koennen.
	 *
	 * @return Das aktive Physik-Objekt.
	 */
	public static final Physik getPhysik () {
		if (physik == null) {
			physik = new Physik();
		}
		return physik;
	}

	/**
	 * Meldet einen Passivator an.
	 *
	 * @param p
	 * 		Der Passivator, der anzumelden ist.
	 */
	public void passivAnmelden (Passivator p) {
		passive.add(p);
	}

	/**
	 * Meldet einen Passivator wieder ab - Vorausgesetzt er war auch angemeldet.
	 *
	 * @param p
	 * 		Der abzumeldende Passivator
	 */
	public void passivAbmelden (Passivator p) {
		passive.remove(p);
	}

	/**
	 * Meldet einen Gravitator fuer Aktiv-Objekte an.
	 *
	 * @param g
	 * 		Der anzumeldende Gravitator
	 */
	public void aktivAnmelden (Gravitator g) {
		gravitatoren.add(g);
	}

	/**
	 * Meldet einen Gravitator wieder ab - Vorausgesetzt er war auch angemeldet.
	 *
	 * @param g
	 * 		Der abzumeldende Gravitator
	 */
	public void aktivAbmelden (Gravitator g) {
		gravitatoren.remove(g);
	}

	/**
	 * Setzt alle Aktiv-Objekte, die eine bestimmte Flaeche uebertreten, in einen Knoten.
	 *
	 * @param k
	 * 		In diesen Knoten werden alle Aktiv-Objekte, die die Flaeche betreten eingefuegt.
	 * @param b
	 * 		Dieses BoundingRechteck beschreibt die kritische Flaeche.
	 */
	public synchronized void alleAktivenEinsetzen (Knoten k, BoundingRechteck b) {
		for (Gravitator g : gravitatoren) {
			if (g.ziel().inFlaeche(b)) {
				k.add(g.ziel());
			}
		}
	}

	/**
	 * Setzt alle Aktiv-Objekte, die eine bestimmte Flaeche uebertreten, nicht jedoch nach der
	 * Verschiebung ein Passiv-Objekt schneiden, in einen Knoten.
	 *
	 * @param k
	 * 		In diesen Knoten werden alle Aktiv-Objekte, die die Flaeche betreten, nicht jedoch nach der
	 * 		Verschiebung problematisch waeren eingefuegt.
	 * @param b
	 * 		Dieses BoundingRechteck beschreibt die kritische Flaeche.
	 * @param v
	 * 		Die kritische Verschiebung.
	 */
	public synchronized void alleAktivenTestenUndEinsetzen (Knoten k, BoundingRechteck b, Vektor v) {
		for (Gravitator g : gravitatoren) {
			if (g.ziel().inFlaeche(b) && !inPassivem(g.ziel().dimension().verschobeneInstanz(v))) {
				k.add(g.ziel());
			}
		}
	}

	/**
	 * Prueft, ob eine Flaeche ein Passiv-Objekt schneidet.
	 *
	 * @param r
	 * 		Die Flaeche der Ueberprueftung, als BoundingRechteck
	 *
	 * @return <code>true</code>, wenn diese Flaeche ein Passivobjekt schneidet, sonst
	 * <code>false</code>.
	 */
	public synchronized boolean inPassivem (BoundingRechteck r) {
		for (Passivator p : passive) {
			if (p.in(r)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Setzt alle Aktiv-Objekte, die eine bestimmte Flaeche uebertreten, nicht jedoch nach der
	 * Verschiebung ein Passiv-Objekt schneiden - mit einer bestimmten Ausnahme - in einen Knoten.
	 *
	 * @param k
	 * 		In diesen Knoten werden alle Aktiv-Objekte, die die Flaeche betreten, nicht jedoch nach der
	 * 		Verschiebung problematisch waeren eingefuegt.
	 * @param b
	 * 		Dieses BoundingRechteck beschreibt die kritische Flaeche.
	 * @param v
	 * 		Die kritische Verschiebung.
	 * @param p
	 * 		Die eine Ausnahme als Passivator
	 */
	public synchronized void alleAktivenTestenUndEinsetzenOhne (Knoten k, BoundingRechteck b, Vektor v, Passivator p) {
		for (Gravitator g : gravitatoren) {
			if (g.ziel().inFlaeche(b) && !inPassivemAusser(g.ziel().dimension().verschobeneInstanz(v), p)) {
				k.add(g.ziel());
			}
		}
	}

	/**
	 * Prueft, ob eine Flaeche ein Passiv-Objekt - bis auf eine Ausnahme schneidet.
	 *
	 * @param r
	 * 		Die Flaeche der Ueberprueftung, als BoundingRechteck
	 * @param aus
	 * 		Die eine Ausnahme, die bei den Kollisionstests nicht beruecksichtigt wird.
	 *
	 * @return <code>true</code>, wenn diese Flaeche ein Passivobjekt - ausser der einen Ausnahme -
	 * schneidet, sonst <code>false</code>.
	 */
	public synchronized boolean inPassivemAusser (BoundingRechteck r, Passivator aus) {
		for (Passivator p : passive) {
			if (p.equals(aus)) {
				continue;
			}
			if (p.in(r)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Gibt die Verschiebung zurueck, die noetig waere um das geblockte Bounding-Rechteck aus seinem
	 * Zustand des Passiv-Blockiertseins zu loesen.
	 *
	 * @param r
	 * 		Das zu entblockende BoundingRechteck
	 *
	 * @return Die Verschiebung, die noetig waere, um das BoundingRechteck aus dem
	 * Passiv-Geblocktsein zu Loesen. Hat die Werte (0|0) fuer den Fall, dass das Bounding-Rechteck
	 * gar nicht passiv blockiert ist.
	 */
	public synchronized Vektor entblocken (BoundingRechteck r) {
		for (Passivator p : passive) {
			if (p.in(r)) {
				float x = 0, y = 0;
				BoundingRechteck pas = p.ziel().dimension();
				if (pas.y < r.y && pas.y + pas.hoehe > r.y + r.hoehe) {
					// X
					if (r.x > pas.x) {
						x = pas.x + pas.breite - r.x;
					} else {
						x = pas.x - (r.x + r.breite);
					}
				} else {
					// Y
					if (r.y > pas.y) {
						y = pas.y + pas.hoehe - r.y;
					} else {
						y = pas.y - (r.y + r.hoehe);
					}
				}
				Vektor retA = new Vektor(x, y);
				return retA.summe(this.entblocken(r.verschobeneInstanz(retA), retA));
			}
		}
		return Vektor.NULLVEKTOR;
	}

	/**
	 * Die Interne Block methode zum garantierten entblocken eines BoundingRechtecks OHNE
	 * StackOverflow.
	 *
	 * @param r
	 * 		Das zu entblockende BR
	 * @param letzte
	 * 		Die letzte Verschiebung (aus der Methode <code>entblocken(Vektor)</code>)
	 *
	 * @return Die noch noetige Verschiebung, um das Bounding-Rechteck sicher zu entblocken.
	 *
	 * @see #entblocken(BoundingRechteck)
	 */
	private synchronized Vektor entblocken (BoundingRechteck r, Vektor letzte) {
		for (Passivator p : passive) {
			if (p.in(r)) {
				float x = 0, y = 0;
				BoundingRechteck pas = p.ziel().dimension();
				if (letzte.x != 0) {
					// X
					if (letzte.x > 0) {
						x = pas.x + pas.breite - r.x;
					} else {
						x = pas.x - (r.x + r.breite);
					}
				} else {
					// Y
					if (letzte.y > 0) {
						y = pas.y + pas.hoehe - r.y;
					} else {
						y = pas.y - (r.y + r.hoehe);
					}
				}
				Vektor retA = new Vektor(x, y);
				return retA.summe(this.entblocken(r.verschobeneInstanz(retA), retA));
			}
		}
		return Vektor.NULLVEKTOR;
	}

	/**
	 * Vereinfachte Form der anmelden()-Methode fuer Kollisionstests.<br /> Hierbei wird immer der
	 * Code </code>code = 0</code> bei der <code>reagieren()</code>-Methode mitgegeben, somit ist
	 * beim Aufruf dieser Methode <b>keine Fallunterscheidung innerhalb eines
	 * <code>FallReagierbar</code>-Objektes moeglich!</b>
	 *
	 * @param k
	 * 		Das KollisionsReagierbar-Objekt, das benachrichtigt wird, wenn beide Objekte kollidieren
	 * @param r1
	 * 		Der erste Raum-Teil. Kollidieren beide, so wird das KollisionsReagierbar-Objekt
	 * 		benachrichtigt
	 * @param r2
	 * 		Der zweite Raum-Teil. Kollidieren beide, so wird das KollisionsReagierbar-Objekt
	 * 		benachrichtigt
	 *
	 * @see #anmelden(KollisionsReagierbar, Raum, Raum, int)
	 */
	public void anmelden (KollisionsReagierbar k, Raum r1, Raum r2) {
		anmelden(k, r1, r2, 0);
	}

	/**
	 * Meldet ein KollisionsReagierbar-Interface bei der Physik an. Zusammen mit den 2 auf Kollision
	 * zu ueberwachenden Raum-Objekten sowie dem Code, der bei dem Aufruf beim Treffer zwischen den
	 * beiden mitgegeben werden soll.<br /> <br /> <br /> <br />
	 * <p/>
	 * Die <code>kollision(int code)</code>-Methode des anzumeldenden <code>KollisionsReagierbar</code>-Interfaces
	 * wird ab sofort immer dann aufgerufen wenn:<br /> 1. beide Raum-Objekte schneiden<br /> und 2.
	 * beide Raum-Objekte sichtbar sind<br /> <br /> <br /> <br /> <br /> <br />
	 * <p/>
	 * Diese Methode wird solange immer wieder aufgerufen, wie die Kollision besteht! Wird also in
	 * der <code>kollision(int code)</code>-Methode nicht dafuer gesorgt, dass sich die Objekte
	 * nicht mehr schneiden, <b>so wird diese Methode wieder und wieder aufgerufen!</b> <br /> <br
	 * /> <br /> <br /> <br /> <br />
	 * <p/>
	 * <b>ACHTUNG</b><br /> Es sollten niemals 2 Knoten oder Geometrie (oder ein Knoten und ein
	 * Geometrie) - Objekte gleichzeitig angemeldet werden. Ist dies der Fall, ist der
	 * Kollisionstest nicht so genau, wie er seien koennte.
	 *
	 * @param k
	 * 		Das KollisionsReagierbar-Objekt, das benachrichtigt wird, wenn beide Objekte kollidieren
	 * @param r1
	 * 		Der erste Raum-Teil. Kollidieren beide, so wird das KollisionsReagierbar-Objekt
	 * 		benachrichtigt
	 * @param r2
	 * 		Der zweite Raum-Teil. Kollidieren beide, so wird das KollisionsReagierbar-Objekt
	 * 		benachrichtigt
	 * @param code
	 * 		Der Code, der <b>dem <code>KollisionsReagierbar</code>-Objekt als Parameter in seiner
	 * 		reagieren()-Methode mitgegeben werden soll</b>.
	 */
	public void anmelden (KollisionsReagierbar k, Raum r1, Raum r2, int code) {
		if (r2 instanceof Knoten) {
			Raum r = r1;
			r1 = r2;
			r2 = r;
		}
		kollisionsListe.add(new Auftrag(r1, r2, k, code));
	}

	/**
	 * Sorgt dafuer, das saemtliche Kollsiionsueberwachungsauftraege eines
	 * <code>KollisionsReagierbar</code>-Interfaces nicht mehr ausgefuehrt werden.
	 *
	 * @param k
	 * 		Das Interface, an dem jede Ueberwachung von Raum-Objekten abgebrochen werden soll.
	 */
	public void entfernen (KollisionsReagierbar k) {
		ArrayList<Auftrag> out = new ArrayList<>();
		for (Auftrag a : kollisionsListe) {
			if (a.benachrichtigt(k)) {
				out.add(a);
			}
		}
		for (Auftrag a : out) {
			kollisionsListe.remove(a);
		}
	}

	/**
	 * In diesem Tick findet ein DELTA-t der Physik statt (= 1ms).
	 */
	@Override
	public void tick () {
		for (Gravitator g : gravitatoren) {
			g.tick(runde);
		}
		if (runde == 10) {
			runde = 1;
		} else {
			runde++;
		}
	}

	// <editor-fold defaultstate="collapsed" desc="KollisionReagierbar">

	/**
	 * Die Auftraege, der Kollisionstests
	 */
	private final class Auftrag {
		//

		/**
		 * Kollisionspartner eins/Ausgangspunkt der Kollisionstests
		 */
		private final Raum r1;

		/**
		 * Kollisionspartner 2
		 */
		private final Raum r2;

		/**
		 * Der Listener
		 */
		private final KollisionsReagierbar listener;

		/**
		 * Der Code dieses Auftrags
		 */
		private final int code;

		/**
		 * Konstruktor
		 *
		 * @param r1
		 * 		Koll-Data 1
		 * @param r2
		 * 		Koll-Data 2
		 * @param k
		 * 		Listener
		 * @param code
		 * 		Code
		 */
		public Auftrag (Raum r1, Raum r2, KollisionsReagierbar k, int code) {
			this.r1 = r1;
			this.r2 = r2;
			listener = k;
			this.code = code;
		}

		/**
		 * Fuert einen Kollisionstest durch
		 */
		public void test () {
			if (r1.schneidet(r2) && r1.sichtbar() && r2.sichtbar()) {
				listener.kollision(code);
			}
		}

		/**
		 * @return TRUE, wenn dieser Listener benachrichtigt wird
		 */
		public boolean benachrichtigt (KollisionsReagierbar k) {
			return listener == k;
		}
	}// </editor-fold>




}
